﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace GeneralServices.Model
{
    /// <summary>
    /// Classe implémentant l'interface IRepository.
    /// Correspond au niveau le plus base de l'implémentation du pattern DAO.
    /// Permet d'effectuer des opérations en base de données en retournant des objets DTO utilisable dans l'application.
    /// </summary>
    /// <typeparam name="T">Classe Bean (Image d'une table de la base)</typeparam>
    public class BaseRepository<T> : IRepository<T, IDTO>
        where T : class 
    {
        #region Attributs
        
        #endregion

        #region Propriétés
        /// <summary>
        /// Pattern UnitOfWork permet de committer en base de données que des instances de T.
        /// </summary>
        public IUnitOfWork<T> UnitOfWork
        {
            get;
            protected set;
        }

        /// <summary>
        /// Factory permettant de créer des objets DTO à partir de Bean ou l'inverse.
        /// </summary>
        public AbstractFactoryDTO Factory
        {
            get;
            protected set;
        }

        /// <summary>
        /// Donnés présentes en BDD.
        /// </summary>
        public DbSet<T> Data
        {
            get
            {
                return (UnitOfWork as UnitOfWork<T>).Db.Set<T>();
            }
        }

        /// <summary>
        /// Taille du DbSet.
        /// </summary>
        public int Count
        {
            get
            {
                return Data.Count();
            }
        }
        
        /// <summary>
        /// Retourne le context du modèle.
        /// </summary>
        public DbContext ContextBDD
        {
            get
            {
                return (UnitOfWork as UnitOfWork<T>).Db;
            }
        }

        #endregion

        #region Constructeur

        /// <summary>
        /// Constructeur complet.
        /// </summary>
        /// <param name="factory">Factory permettant d'implémenter le pattern Adapter</param>
        /// <param name="context">Context de la base de données</param>
        public BaseRepository(AbstractFactoryDTO factory, DbContext context)
        {
            UnitOfWork = new UnitOfWork<T>(context);
            Factory = factory;
        }

        #endregion
       
        #region IRepository<T,IDTO> Membres

        public virtual IDTO First(Func<IDTO, bool> predicate)
        {
            return Data.ToList()
                .Select(item => Factory.CreateDTO(item))
                .Where(predicate)
                .First();
        }

        public virtual IEnumerable<IDTO> All()
        {
            return Data.ToList()
                .Select(item => Factory.CreateDTO(item));
        }

        public virtual IDTO Find(object[] key)
        {
            return Factory.CreateDTO(Data.Find(key));
        }

        public virtual IDTO Find(Predicate<IDTO> predicate)
        {
            return Data.ToList()
                .Select(item => Factory.CreateDTO(item))
                .ToList().Find(predicate);
        }

        public virtual IEnumerable<IDTO> Filter(Func<IDTO, bool> predicate)
        {
            return Data.ToList()
                .Select(item => Factory.CreateDTO(item))
                .Where(predicate).AsQueryable();
        }

        public virtual bool Contains(Func<IDTO, bool> predicate)
        {
            return Data.ToList()
                .Select(item => Factory.CreateDTO(item))
                .Where(predicate).Count() > 0;
        }

        public virtual IDTO SingleOrDefault(Func<IDTO, bool> predicate)
        {
            try
            {
                return this.First(predicate);
            }
            catch (Exception) { return null;}
        }

        public virtual T Update(IDTO entity, Boolean commit = false)
        {
            try
            {
                object obj = Factory.CreateBean(entity);
                T t = Data.Find((obj as EntityModel).GetPrimaryKey());

                if (MergeObject(t, obj))
                {

                    DbEntityEntry<T> entry = (UnitOfWork as UnitOfWork<T>).Db.Entry(t);
                    entry.State = EntityState.Modified;
                    //Data.Attach(obj as T);

                    if (commit)
                        (UnitOfWork as UnitOfWork<T>).Commit();
                }

                return t;
            }
            catch (Exception) { return null; }
        }

        public virtual int Delete(IDTO entity, Boolean commit = false)
        {
            T t = GetElementInBDD(entity);

            Data.Remove(t);

            if (commit)
                return (UnitOfWork as UnitOfWork<T>).Commit();
            else
                return 1;
        }

        public virtual int Delete(Func<IDTO, bool> predicate, Boolean commit = false)
        {
            foreach (object obj in Factory.CreateListBean(Filter(predicate).ToList()))
            {
                T t = Data.Find((obj as EntityModel).GetPrimaryKey());
                Data.Remove(t);
            }

            if (commit)
                return (UnitOfWork as UnitOfWork<T>).Commit();
            else
                return Filter(predicate).ToList().Count;
        }

        public virtual T Insert(IDTO entity, Boolean commit = false)
        {
            try
            {
                T newEntry = Data.Add(Factory.CreateBean(entity) as T);

                if (commit)
                    (UnitOfWork as UnitOfWork<T>).Commit();

                return newEntry;
            }
            catch (Exception e) { return null; }
        }

        public virtual T Save(IDTO t)
        {
            if (t == null)
                return null;

            T element = GetElementInBDD(t);
            DbEntityEntry<T> entry = (UnitOfWork as UnitOfWork<T>).Db.Entry(element);
            if (element == null || entry.State == EntityState.Added)
                return Insert(t);
            else if (entry.State != EntityState.Deleted || entry.State != EntityState.Detached)
            {
                Update(t);
                return element;
            }
            else
                return element;
        }

        public virtual void Refresh(IDTO TObject)
        {
            T element = GetElementInBDD(TObject);
            DbEntityEntry<T> entry = (UnitOfWork as UnitOfWork<T>).Db.Entry(element);

            if (entry.State == EntityState.Added)
                return;
            
            entry.Reload();
        }

        public virtual void RollBack(IDTO entity)
        {
            (UnitOfWork as UnitOfWork<T>).RollBack(Factory.CreateBean(entity) as T);
        }
        
        public virtual IDTO CreateEmpty()
        {
            return Factory.CreateDTO(Data.Create());
        }

        public virtual void RollBackAll()
        {
            (UnitOfWork as UnitOfWork<T>).RollBackAll();
        }

        #endregion
       
        #region Méthodes Protégés

        /// <summary>
        /// Return null if dto not exist
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        protected virtual T GetElementInBDD(IDTO dto)
        {
            object obj = Factory.CreateBean(dto);
            try
            {
                T t = Data.Find((obj as EntityModel).GetPrimaryKey());
                return t;
            }
            catch (Exception) { return null; }
        }

        /// <summary>
        /// Permet de modifier les propriétés de l'objet original par l'objet copy qui a été modifié par l'utilisateur.
        /// </summary>
        /// <param name="original"></param>
        /// <param name="copy"></param>
        /// <returns></returns>
        protected virtual Boolean MergeObject(Object original, Object copy)
        {
            Boolean _isChanged = false;
            if (original.GetType() == copy.GetType())
            {
                Type t = original.GetType();
                PropertyInfo[] properties = t.GetProperties();

                foreach (PropertyInfo p in properties)
                {
                    if (copy.GetType().GetProperties().Select(prop => prop.Name).Contains(p.Name))
                    {
                        Object valueOriginal = p.GetValue(original);
                        Object valueCopy = p.GetValue(copy);

                        if ((valueOriginal == null && valueCopy != null) ||
                            (valueOriginal != null && !valueOriginal.Equals(valueCopy)))
                        {
                            p.SetValue(original, valueCopy);
                            _isChanged = true;
                        }
                    }
                }
            }
            else
                throw new Exception("Les deux objets métiers n'ont pas le même type.");

            return _isChanged;
        }

        #endregion
    }
}
